<?php
/**
 * Created by PhpStorm.
 * Script Name: SocketServer.phpphp
 * Create: 2019/8/4 14:50
 * Description: 聊天websocket交互控制器
 * Author: Jason<dcq@kuryun.cn>
 */
namespace app\common\coroutine;

use think\swoole\Server;
use think\Db;

class SocketServer extends Server
{
    protected $host = '0.0.0.0';
    protected $port = 9501;
    protected $serverType = 'socket';
    protected $option = [
        'worker_num'    => 4,
        'daemonize'	    => true, //守护进程（上线改为true）
        'backlog'	    => 128,
        'dispatch_mode' => 2,
        'heartbeat_check_interval' => 60, //心跳检测：每60秒遍历所有连接
        'heartbeat_idle_time' => 3600,  //在页面一小时未说话则字段关闭连接
        'user' => 'www',
        'group' => 'www'
    ];
    protected $redis;
    protected $groupsMsgContentM;
    protected $groupsMsgUserToUserM;
    protected $keywordM;
    protected $weixinM;
    protected $weixinUserM;
    protected $groupsToUserM;
    protected $userGroupsM;
    protected $managerM;
    protected $keywords = [];  //关键词列表
    protected $weixinInfo = [];  //微信信息

    /**
     * 创建redis连接对象
     * @param $server
     * @author Jason<dcq@kuryun.cn>
     */
    public function onWorkerStart($server) {
        $redis = new \think\cache\driver\Redis(config('cache.redis'));
        $this->redis = $redis->handler();
        $this->groupsMsgContentM = model('GroupsMsgContent');
        $this->groupsMsgUserToUserM = model('GroupsMsgUserToUser');
        $this->keywordM = model('Keyword');
        $this->weixinM = model('Weixin');
        $this->weixinUserM = model('WeixinUser');
        $this->groupsToUserM = model('GroupsToUser');
        $this->userGroupsM = model('UserGroups');
        $this->managerM = model('Manager');
    }

    /**
     * 建立连接时回调函数，连接传2个参数（weixin_id, groups_user_id）
     * @param object $server
     * @param object $req
     * @author Jason<dcq@kuryun.cn>
     */
    public function onOpen($server, $req) {
        //客户端标识
        $fd = $req->fd;
        $groups_user_id = $req->get['groups_user_id'];
        $weixin_id = $req->get['weixin_id'];
        //存储客户端参数信息
        $groups_to_user = $this->groupsToUserM->getOneByMap(['id' => $groups_user_id, 'weixin_id' => $weixin_id], 1);
        $groups = $this->userGroupsM->getOneByMap(['id' => $groups_to_user['group_id'], 'weixin_id' => $weixin_id], 1);
        $member_info = [
            'weixin_id'     => $weixin_id,
            'username'      => $groups_to_user['username'],
            'headimgurl'    => $groups_to_user['headimgurl'],
            'class_id'      => $groups['class_id'],
            'user_id'       => $groups_to_user['user_id'],
            'group_id'      => $groups_to_user['group_id'],
            'user_type'     => $groups_to_user['user_type']
        ];
        $this->redis->set('member_info::' . $fd, json_encode($member_info));
        //获取关键词列表
        $this->keywords = $this->keywordM->getAll(['weixin_id' => $weixin_id, 'status' => 1], ['id' => 'desc'], true, 1);
        //获取微信信息
        $this->weixinInfo = $this->weixinM->getOne($weixin_id);
        //更新化未读消息值
        $user_unread = 'unread::'.$groups_to_user['user_id'].'::' . $groups_to_user['user_type'];
        $group_unread = 'unread::'.$groups_to_user['user_id'].'::' . $groups_to_user['group_id'].'::'.$groups_to_user['user_type'];
        if($this->redis->exists($user_unread)) {
            if($this->redis->exists($group_unread)) {
                $group_unread_value = $this->redis->get($group_unread);
                //总数减少该群的未读数
                $this->redis->decrby($user_unread, $group_unread_value);
                $this->redis->set($group_unread, 0);
            }
        }
        echo "用户{$groups_to_user['user_id']}建立了连接,标识为{$fd}\n";
    }

    /**
     * 接收数据时回调函数
     * @param object $server
     * @param object $frame
     * @author Jason<dcq@kuryun.cn>
     */
    public function onMessage($server, $frame) {
        $fd = $frame->fd;
        $message = json_decode($frame->data, true);
        $member_info = json_decode($this->redis->get('member_info::' . $fd), true);
        //消息类型分发处理
        switch ($message['flag']) {
            case 'group_init':
                $this->groupInit($server, $frame, $member_info, $message);
                break;
            case 'group_history':
                $this->groupHistory($server, $frame, $member_info, $message);
                break;
            case 'group_chat':
                $this->groupChat($server, $frame, $member_info, $message);
                break;
            case 'withdraw':  //撤回
                $this->groupWithdraw($server, $frame, $member_info, $message);
                break;
        }
    }

    /**
     * 连接关闭时回调函数
     * @param object $server
     * @param object $fd
     * @author Jason<dcq@kuryun.cn>
     */
    public function onClose($server, $fd) {
        echo "标识{$fd}关闭了连接\n";
    }

    /**
     * 群消息初始化处理
     * @param object $server
     * @param object $frame
     * @param array $member_info
     * @param array $message
     * @author Jason<dcq@kuryun.cn>
     */
    private function groupInit($server, $frame, $member_info, $message) {
        $map = [
            'weixin_id' => $member_info['weixin_id'],
            'group_id' => $member_info['group_id'],
            'class_id' => $member_info['class_id'],
        ];
        if($message['id']) {
            $map['id'] = ['lt', $message['id']];
        }

        //进入某群聊初始化，推送该群最新30条信息
        $list = $this->groupsMsgContentM->getList([1, 60], $map, ['id' => 'desc'], true, 1);
        if($list) {
            //从60条中筛选30条推送（保证用户可一直查看到自己发送的带敏感词记录）
            $push_list = [];
            $count = 0;
            foreach ($list as $item) {
                if($count < 30) {
                    if($item['status']) {
                        array_push($push_list, $item);
                    }else {
                        //敏感记录判断是否是当前用户的
                        if($member_info['user_id'] == $item['from_user_id']) {
                            array_push($push_list, $item);
                        }
                    }
                }
                $count++;
            }
            //反转数组
            $push_list = array_reverse($push_list);
            $list = [];
            foreach ($push_list as $content) {
                $tmp = [
                    'code'  => 2,  //初始化数据状态码
                    'msg'   => 'success',
                    'data'  => $content,
                    'online' => count($server->connections)
                ];
                array_push($list, $tmp);
            }
            $server->push($frame->fd, json_encode($list));
        }else {
            $result = [
                'code'  => 2,  //初始化数据状态码
                'msg'   => 'success',
                'data'  => null,
                'online' => count($server->connections)
            ];
            $server->push($frame->fd, json_encode([$result]));
        }
    }

    /**
     * 历史消息记录
     * @param object $server
     * @param object $frame
     * @param array $member_info
     * @param array $message
     * @author Jason<dcq@kuryun.cn>
     */
    private function groupHistory($server, $frame, $member_info, $message) {
        $map = [
            'weixin_id' => $member_info['weixin_id'],
            'group_id' => $member_info['group_id'],
            'class_id' => $member_info['class_id'],
            'id' => ['lt', $message['id']],
        ];
        if($message['direction'] == 'down') {
            $map['id'] = ['gt', $message['id']];
        }

        //向上加载更多历史记录
        $list = $this->groupsMsgContentM->getList([1, 40], $map, ['id' => 'desc'], true, 1);
        if($list) {
            //从40条中筛选20条推送（保证用户可一直查看到自己发送的带敏感词记录）
            $push_list = [];
            $count = 0;
            foreach ($list as $item) {
                if($count < 20) {
                    if($item['status']) {
                        array_push($push_list, $item);
                    }else {
                        //敏感记录判断是否是当前用户的
                        if($member_info['user_id'] == $item['from_user_id']) {
                            array_push($push_list, $item);
                        }
                    }
                }
                $count++;
            }
            $list = [];
            foreach ($push_list as $content) {
                $tmp = [
                    'code'  => 3,  //获取历史记录状态码
                    'msg'   => 'success',
                    'data'  => $content,
                    'online' => count($server->connections)
                ];
                array_push($list, $tmp);
            }
            $server->push($frame->fd , json_encode($list));
        }
    }

    /**
     * 群聊消息
     * @param object $server
     * @param object $frame
     * @param array $member_info
     * @param array $message
     * @author Jason<dcq@kuryun.cn>
     */
    private function groupChat($server, $frame, $member_info, $message) {
        $fd = $frame->fd;
        $content = [
            'weixin_id'     => $member_info['weixin_id'],
            'class_id'      => $member_info['class_id'],
            'from_user_id'  => $member_info['user_id'],
            'from_username' => $member_info['username'],
            'from_headimgurl' => $member_info['headimgurl'],
            'content'       => $message['content'],
            'group_id'      => $member_info['group_id'],
            'msg_type'      => $message['msg_type'],
            'user_type'     => $message['user_type'],
            'status'        => 1, //默认允许显示
            'create_time'   => time()
        ];
        //要推送聊天的内容
        $result = [
            'code'  => 1,   //普通聊天状态码
            'msg'   => 'success',
            'data'  => $content,
            'online' => count($server->connections)
        ];
        //文本消息是否包含敏感内容(启动监控)
        if($message['msg_type'] == 1 && $this->weixinInfo['monitor'] && $this->keywords) {
            foreach ($this->keywords as $word) {
                if(strpos($message['content'], $word['title']) !== false) {
                    //包含敏感词，不予显示
                    $content['status'] = 0;
                    $uncheck = 'uncheck::'.$member_info['weixin_id'];
                    //增加处理数
                    $this->redis->incr($uncheck);
                    break;
                }
            }
        }
        //聊天消息入队列
        $this->redis->lpush('group_chat', json_encode($content));

        //当前在线用户微信id
        $user_online_ids = [];
        //消息推送给全部连接用户
        foreach($server->connections as $con) {
            if($server->isEstablished($con)) {
                $member = json_decode($this->redis->get('member_info::' . $con), true);
                if($member_info['group_id'] == $member['group_id']) {
                    if(! $content['status']) {
                        //包含敏感词的消息只推送给发消息之人、园长、教师，并推送模板消息给园长、教师
                        if(in_array($member['user_type'], [2, 3]) || ($con == $fd)) {
                            $server->push($con, json_encode([$result]));
                        }
                        //推送模板消息给园长、教师
                        $this->sentTemplateKeyword($content);
                    }else {
                        $server->push($con, json_encode([$result]));
                        //该群在线的用户微信id
                        array_push($user_online_ids, $member['user_id']);
                    }
                }
            }
        }

        //正常消息
        if($content['status']) {
            //当群中有发出消息与上一条消息间隔30分钟以上时，对于所有未在网页中人员进行提醒
            $this->sendTemplateMsg($member_info, $message, $user_online_ids);
            //增加未在线成员未读消息数
            $this->unreadMsg($user_online_ids, $member_info['group_id'], $member_info['weixin_id']);
        }
    }

    /**
     * 群聊消息撤回
     * @param object $server
     * @param object $frame
     * @param array $member_info
     * @param array $message
     * @author Jason<dcq@kuryun.cn>
     */
    private function groupWithdraw($server, $frame, $member_info, $message) {
        $message = $message['content'];
        $diff = time() - $message['create_time'];
        //三分钟内允许撤回
        if(($diff < 180) && ($member_info['user_id'] == $message['from_user_id'])) {
            //删除数据库数据
            $map = [
                'weixin_id'     => $member_info['weixin_id'],
                'class_id'      => $member_info['class_id'],
                'from_user_id'  => $member_info['user_id'],
                'content'       => $message['content'],
                'group_id'      => $member_info['group_id'],
                'msg_type'      => $message['msg_type'],
                'user_type'     => $message['user_type'],
            ];
            if($msg = $this->groupsMsgContentM->getOneByMap($map, 1)) {
                $this->groupsMsgContentM->delOne(['weixin_id' => $member_info['weixin_id'], 'id' => $msg['id']]);
            }
            //删除队列数据
            if($this->redis->exists('group_chat')) {
                if($this->redis->llen('group_chat')) {
                    $value = [
                        'weixin_id'     => $message['weixin_id'],
                        'class_id'      => $message['class_id'],
                        'from_user_id'  => $message['from_user_id'],
                        'from_username' => $message['from_username'],
                        'from_headimgurl' => $message['from_headimgurl'],
                        'content'       => $message['content'],
                        'group_id'      => $message['group_id'],
                        'msg_type'      => $message['msg_type'],
                        'user_type'     => $message['user_type'],
                        'status'        => $message['status'], //默认允许显示
                        'create_time'   => $message['create_time']
                    ];
                    $this->redis->lrem('group_chat', json_encode($value), 1);
                }
            }

            $result = [
                'code'  => 4,   //撤回聊天状态码
                'msg'   => 'success',
                'data'  => $message,
                'online' => count($server->connections)
            ];
            //消息推送给全部连接用户，前端显示删除对应聊天记录
            foreach($server->connections as $con) {
                if($server->isEstablished($con)) {
                    $member = json_decode($this->redis->get('member_info::' . $con), true);
                    if($member_info['group_id'] == $member['group_id']) {
                        $server->push($con, json_encode([$result]));
                    }
                }
            }
        }
    }

    /**
     * 30分钟内未在线用户模板消息推送提醒
     * @param array $member_info 当前发消息的用户
     * @param array $message 聊天消息
     * @param array $user_online_ids 群内当前在线用户
     * @author Jason<dcq@kuryun.cn>
     */
    private function sendTemplateMsg($member_info, $message, $user_online_ids) {
        $now = time();
        //当群中有发出消息与上一条消息间隔30分钟以上时，对于所有未在网页中人员进行提醒
        $newest = $this->groupsMsgContentM->getList([1, 1], [
                'weixin_id' => $member_info['weixin_id'],
                'group_id' => $member_info['group_id'],
                'class_id' => $member_info['class_id'], 'status' => 1
            ], ['id' => 'desc'], true, 1
        );
        if($newest) {
            if(($now - $newest[0]['create_time']) >= 1800) {
                //筛选群内未在线且接收消息提醒的用户
                $user = $this->groupsToUserM->getField('user_id, user_type', [
                    'weixin_id' => $member_info['weixin_id'],
                    'user_id' => ['notin', $user_online_ids],
                    'group_id' => $member_info['group_id'],
                    'news_no_interruption' => 0 //未开启免打扰
                ]);

                //当前夜间22点时间戳
                $begin_time = mktime(22, 00, 00, date('m'), date('d'), date('Y'));
                $end_time = $begin_time + 9 * 3600;
                if($now > $begin_time && $now < $end_time) {
                    //筛选群内未在线并过滤设置夜间免打扰用户(22:00--7:00)
                    $user = $this->groupsToUserM->getField('user_id, user_type', [
                        'weixin_id' => $member_info['weixin_id'],
                        'user_id' => ['notin', $user_online_ids], //不在线的群成员
                        'group_id' => $member_info['group_id'],
                        'news_no_interruption' => 0,
                        'night_no_interruption' => 0 //未开启夜间免打扰
                    ]);
                }

                //推送模板消息提醒
                if(! empty($user)) {
                    switch((int)$message['msg_type']) {
                        case 1:
                            $content = $member_info['username'] . ':' . $message['content'];
                            if(mb_strlen($message['content'], "utf-8") > 20) {
                                $content = $member_info['username'] . ':' . mb_substr($message['content'], 0, 20, 'utf8') . '...';
                            }
                            break;
                        case 2:
                            $content = $member_info['username'] . ': 图片';
                            break;
                        case 4:
                            $content = $member_info['username'] . ': 视频';
                            break;
                    }

                    $user_groups = $this->userGroupsM->getOneByMap(['weixin_id' => $member_info['weixin_id'], 'id' => $member_info['group_id']]);
                    foreach ($user as $key => $value) {
                        $url = config('domain') . url('class_group/groupchat', ['group_id' => $member_info['group_id'], 'user_type' => $value, 'wid' => $member_info['weixin_id']]);
                        $tip = '家长您好，'. $user_groups['title'] .'有新消息，请查阅';
                        if(in_array($value, [2, 3])) {
                            $tip = '老师您好，'. $user_groups['title'] .'有新消息，请查阅';
                        }
                        //推送内容
                        $message = [
                            'do'        => 'notice_service',
                            'touser'    => $key,
                            'url'       => $url,
                            'first'     => $tip,
                            'keyword1'  => $content,
                            'keyword2'  => date('Y-m-d H:i', time()),
                            'keyword3'  => '消息内容，请点击详情查看',
                            'remark'    => '发信人：' . $user_groups['title'] . ' ' . $member_info['username']
                        ];
                        $this->redis->rpush('notice_service', json_encode($message));
                    }
                }
            }
        }
    }

    /**
     * 敏感词推送模板消息给园长、教师
     * @param array $content
     * @author Jason<dcq@kuryun.cn>
     */
    private function sentTemplateKeyword($content) {
        //查询该校区的园长，该班级的老师老师
        $to_master_ids = $this->managerM->getField('user_id', ['weixin_id' => $content['weixin_id'], 'role' => 1]);
        $to_teacher_ids = $this->managerM->getField('user_id', ['weixin_id' => $content['weixin_id'], 'class_id' => $content['class_id'], 'role' => 2]);
        $user_ids = array_merge($to_master_ids, $to_teacher_ids);
        $user_ids = array_unique($user_ids);
        $user_groups = $this->userGroupsM->getOneByMap(['weixin_id' => $content['weixin_id'], 'id' => $content['group_id']]);
        if(count($user_ids)) {
            foreach ($user_ids as $user_id) {
                $url = config('domain') . url('manage/keywordshield', ['wid' => $content['weixin_id']]);
                //推送内容
                $message = [
                    'do'        => 'notice_service',
                    'touser'    => $user_id,
                    'url'       => $url,
                    'first'     => '老师您好，'. $user_groups['title'] .'中有聊天消息包含敏感词，需审核发布，请查阅',
                    'keyword1'  => $content['content'],
                    'keyword2'  => date('Y-m-d H:i', time()),
                    'keyword3'  => '消息内容，请点击详情查看',
                    'remark'    => '发信人：' .  $user_groups['title'] . ' ' . $content['from_username']
                ];
                $this->redis->rpush('notice_service', json_encode($message));
            }
        }
    }

    /**
     * 非在线成员增加未读消息数
     * @param array $user_online_ids
     * @param int $group_id
     * @param int $weixin_id
     * @author Jason<dcq@kuryun.cn>
     */
    private function unreadMsg($user_online_ids, $group_id, $weixin_id) {
        if(!empty($user_online_ids)) {
            //获取群内未在线成员
            $group_users = $this->groupsToUserM->getAll([
                'user_id' => ['notin', $user_online_ids],
                'group_id' => $group_id,
                'weixin_id' => $weixin_id,
                'status' => 1
            ], ['id' => 'desc']);

            if($group_users) {
                foreach ($group_users as $item) {
                    $user_unread = 'unread::'.$item['user_id'].'::'.$item['user_type'];
                    $group_unread = 'unread::'.$item['user_id'].'::'.$group_id.'::'.$item['user_type'];
                    //增加未读数
                    $this->redis->incr($user_unread);
                    $this->redis->incr($group_unread);
                }
            }
        }
    }
}